package org.dfl.core.model.transitions;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.log4j.Logger;
import org.dfl.core.data.History;
import org.dfl.core.data.PureHistory;
import org.dfl.core.data.Token;
import org.dfl.core.data.HistoryDataMapping;
import org.dfl.core.data.types.Type;
import org.dfl.core.data.types.TypeContainer;
import org.dfl.core.model.IdGenerator;
import org.dfl.core.model.TInputEdge;
import org.dfl.core.model.TOutputEdge;
import org.dfl.core.model.TransitionExecutionException;
import org.dfl.core.model.TransitionFireResult;
import org.dfl.core.model.XMLLoaderException;
import org.dfl.core.utils.Utils;
import org.dfl.messages.Messages;
import org.dfl.plugin.DflPlugin;
import org.embl.ebi.escience.scufl.XScufl;
import org.jdom.Element;

/**
 * abstract transition element - base class for all transitions
 * 
 * @author Piotr Wlodarczyk
 * 
 */
public abstract class Transition {
	private final static Logger logger = Logger.getLogger(Transition.class);
	
	/** namespace to load toolSpecific element from XML Transition element * */
	public static org.jdom.Namespace toolspecificNS = XScufl.XScuflNS;

	// id tranzycji
	protected String id;
	// nazwa tranzycji
	private String name = ""; //$NON-NLS-1$
	// opis tranzycji
	private String description = null;

	// definicja typu wyjscia
	private TypeContainer outputTypeContainer;

	// podlaczone wyjscia
	private Set<TOutputEdge> outputs = new HashSet<TOutputEdge>();

	public Transition() {
		init();
	}

	/**
	 * 
	 */
	protected void init() {
		// set the output type
		outputTypeContainer = new TypeContainer(getDefaultOutputDefinition());
	}

	protected abstract Type getDefaultOutputDefinition();

	/**
	 * pobiera id tranzycji, a jesli nie byl ustalony, to go najpierw generuje
	 * 
	 * @return
	 */
	public final String getId() {
		if (id == null) {
			regenerateId();
		}
		return id;
	}

	public void setId(String id) {
		if (!IdGenerator.isRegistered(id)) {
			IdGenerator.registerId(id);
			this.id = id;
		} else {
			regenerateId();
		}
	}

	/**
	 * generowanie nowego id dla tranzycji
	 * 
	 */
	protected void regenerateId() {
		String possibleId = Utils.stripTags(Utils.getClassName(this.getClass())
				.replaceFirst("Transition$", "")); //$NON-NLS-1$ //$NON-NLS-2$
		id = IdGenerator.getGeneratedId(possibleId);
	}

	// settery/gettery

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	// --------------------------------------------------- IO
	// ------------------------------------------------- //
	protected abstract boolean hasFreeInput(String key);

	/**
	 * pobiera typ wyjscia
	 * 
	 * @return
	 */
	public TypeContainer getOutputTypeContainer() {
		return outputTypeContainer;
	}

	/**
	 * pobiera typ wyjscia
	 * 
	 * @param type
	 */
	public void setOutputType(Type type) {
		outputTypeContainer.setType(type);
		outputTypeContainer.makeOrig();

		if (!getOutputEdges().isEmpty()) {
			getOutputTypeContainer().rebuild();
		}
	}

	/**
	 * Ustawia wyj�cie z tranzycji
	 * 
	 * @param place -
	 *            miejsce gdzie trafi� dane wyj�ciowe
	 * @param edgeProperties -
	 *            w�a�ciwo�ci wyj�cia
	 * @throws Exception
	 */
	public boolean addOutput(TOutputEdge e) throws Exception {
		if (e.getTypeContainer().connectWith(getOutputTypeContainer())) {
			outputs.add(e);
			return true;
		} else {
			return false;
		}
	}

	public void removeOutput(TOutputEdge e) {
		outputs.remove(e);
	}

	public Set<TOutputEdge> getOutputEdges() {
		return new HashSet<TOutputEdge>(outputs);
	}

	public void disconnectAllOutputs() {
		Set<TOutputEdge> os = new HashSet<TOutputEdge>(outputs);
		for (TOutputEdge o : os) {
			o.disconnect();
		}
	}

	// --------------------------------------------------- URUCHAMIANIE
	// ------------------------------------------------- //
	public boolean isReadyToRun() {
		if (areAllInputsDefined() && getInputEdges().isEmpty()) {
			return true;
		}
		
		Map<History, Map<TInputEdge, PureHistory>> notNestedResults = new HashMap<History, Map<TInputEdge, PureHistory>>();
		Map<History, Map<TInputEdge, PureHistory>> nestedResults = new HashMap<History, Map<TInputEdge, PureHistory>>();
		Collection<TInputEdge> nestedEdges = new ArrayList<TInputEdge>();

		// result2 sluzy jedynie do mergowania kolejnych wynikow
		Map<History, Map<TInputEdge, PureHistory>> result2 = null;
		boolean hasNotNestedEdge = false;
		boolean hasNestedEdge = false;

		// getting input data has to be synchronized in all transitions!
		synchronized (DflPlugin.class) {
			Iterator<TInputEdge> inputKeyIterator = getInputEdges().iterator();
			while (inputKeyIterator.hasNext()
					&& (!hasNotNestedEdge || notNestedResults.size() > 0)) {
				result2 = new HashMap<History, Map<TInputEdge, PureHistory>>();

				TInputEdge inputEdge = (TInputEdge) inputKeyIterator.next();
				boolean isNestedEdge = inputEdge.isNested();
				if (isNestedEdge)
					nestedEdges.add(inputEdge);

				Collection<HistoryDataMapping> histories = inputEdge
						.getFixedHistories();
				// jesli choc w 1 wejsciu brakuje danych - nie mozna odpalic
				// tranzycji
				if (!isNestedEdge && histories.isEmpty()) {
					return false;
				}

				for (HistoryDataMapping hdm : histories) {
					PureHistory historyKey = hdm.getPlaceKey();
					History history = hdm.getHistory();

					// jesli pierwszy input, to dodaj do wyniku
					if (!result2.containsKey(history)) {
						Map<TInputEdge, PureHistory> resH = new HashMap<TInputEdge, PureHistory>();
						resH.put(inputEdge, historyKey);
						result2.put(history, resH);
					} else {
						result2.get(history).put(inputEdge, historyKey);
					}
				}

				if (!hasNotNestedEdge && !isNestedEdge) {
					notNestedResults = result2;
					hasNotNestedEdge = true;
				}
				if (!hasNestedEdge && isNestedEdge) {
					nestedResults = result2;
					hasNestedEdge = true;
				} else {
					Map<History, Map<TInputEdge, PureHistory>> resultsToMerge = isNestedEdge ? nestedResults
							: notNestedResults;

					Collection<History> removeHists = new ArrayList<History>(
							result2.keySet());
					removeHists.removeAll(resultsToMerge.keySet());

					// usuwamy te, ktorych wczesniej nie znalezlismy
					for (History h : removeHists) {
						resultsToMerge.remove(h);
					}

					Collection<History> newHists = new ArrayList<History>(
							result2.keySet());
					newHists.removeAll(removeHists);

					// usuwamy te, ktorych wczesniej nie znalezlismy
					for (History h : newHists) {
						if (resultsToMerge.containsKey(h)) {
							for (TInputEdge e : result2.get(h).keySet()) {
								resultsToMerge.get(h).put(e,
										result2.get(h).get(e));
							}
						} else {
							resultsToMerge.put(h, result2.get(h));
						}
					}
				}
			}
		}
		// get the final history and prepare input data
		HashSet<History> historiesToCheck = new HashSet<History>();
		historiesToCheck.addAll(notNestedResults.keySet());
		historiesToCheck.addAll(nestedResults.keySet());
		for (History history : historiesToCheck) {
			if (hasNestedEdge == false
					|| hasNotNestedEdge == false
					|| (nestedResults.containsKey(history) && notNestedResults
							.containsKey(history))) {
				return true;
			} else if (hasNestedEdge && hasNotNestedEdge && history.isSpecial()
					&& history.getCol().isEmpty()) {
				return true;
			} else if (hasNestedEdge && hasNotNestedEdge && history.isSpecial()
					&& !history.getCol().isEmpty()
					&& nestedResults.containsKey(history.getHistory())) {
				return true;
			}
		}

		return false;
	}

	/**
	 * m�wi, czy wszystkie zmienne s� ju� zainicjowane = gotowo�� do "odpalenia"
	 * 
	 * @return true je�li mo�na odpali� t� tranzycj�
	 * @throws TransitionExecutionException
	 */
	@SuppressWarnings("unchecked")
	public final TransitionFireResult initRun() {
		if (areAllInputsDefined() && getInputEdges().isEmpty()) {
			return tryToRunWithNoInput();
		}

		Map<String, Object> fireInputDataMap = new HashMap<String, Object>();
		;
		History hist = null;
		TransitionFireResult transitionFireResult = new TransitionFireResult(
				this);

		try {
			Map<History, Map<TInputEdge, PureHistory>> notNestedResults = new HashMap<History, Map<TInputEdge, PureHistory>>();
			Map<History, Map<TInputEdge, PureHistory>> nestedResults = new HashMap<History, Map<TInputEdge, PureHistory>>();
			Collection<TInputEdge> nestedEdges = new ArrayList<TInputEdge>();

			// result2 sluzy jedynie do mergowania kolejnych wynikow
			Map<History, Map<TInputEdge, PureHistory>> result2 = null;
			boolean hasNotNestedEdge = false;
			boolean hasNestedEdge = false;
			Iterator<TInputEdge> inputKeyIterator = getInputEdges().iterator();

			// getting input data has to be synchronized in all transitions!
			synchronized (DflPlugin.class) {
				while (inputKeyIterator.hasNext()
						&& (!hasNotNestedEdge || notNestedResults.size() > 0)) {
					result2 = new HashMap<History, Map<TInputEdge, PureHistory>>();

					TInputEdge inputEdge = (TInputEdge) inputKeyIterator.next();
					boolean isNestedEdge = inputEdge.isNested();
					if (isNestedEdge)
						nestedEdges.add(inputEdge);

					Collection<HistoryDataMapping> histories = inputEdge
							.getFixedHistories();
					// jesli choc w 1 wejsciu brakuje danych - nie mozna odpalic
					// tranzycji
					if (!isNestedEdge && histories.isEmpty()) {
						return transitionFireResult;
					}

					for (HistoryDataMapping hdm : histories) {
						PureHistory historyKey = hdm.getPlaceKey();
						History history = hdm.getHistory();

						// jesli pierwszy input, to dodaj do wyniku
						if (!result2.containsKey(history)) {
							Map<TInputEdge, PureHistory> resH = new HashMap<TInputEdge, PureHistory>();
							resH.put(inputEdge, historyKey);
							result2.put(history, resH);
						} else {
							result2.get(history).put(inputEdge, historyKey);
						}
					}

					if (!hasNotNestedEdge && !isNestedEdge) {
						notNestedResults = result2;
						hasNotNestedEdge = true;
					}
					if (!hasNestedEdge && isNestedEdge) {
						nestedResults = result2;
						hasNestedEdge = true;
					} else {
						Map<History, Map<TInputEdge, PureHistory>> resultsToMerge = isNestedEdge ? nestedResults
								: notNestedResults;

						Collection<History> removeHists = new ArrayList<History>(
								result2.keySet());
						removeHists.removeAll(resultsToMerge.keySet());

						// usuwamy te, ktorych wczesniej nie znalezlismy
						for (History h : removeHists) {
							resultsToMerge.remove(h);
						}

						Collection<History> newHists = new ArrayList<History>(
								result2.keySet());
						newHists.removeAll(removeHists);

						// usuwamy te, ktorych wczesniej nie znalezlismy
						for (History h : newHists) {
							if (resultsToMerge.containsKey(h)) {
								for (TInputEdge e : result2.get(h).keySet()) {
									resultsToMerge.get(h).put(e,
											result2.get(h).get(e));
								}
							} else {
								resultsToMerge.put(h, result2.get(h));
							}
						}
					}
				}

				// get the final history and prepare input data
				HashSet<History> historiesToCheck = new HashSet<History>();
				historiesToCheck.addAll(notNestedResults.keySet());
				historiesToCheck.addAll(nestedResults.keySet());
				for (History history : historiesToCheck) {
					if (hasNestedEdge == false
							|| hasNotNestedEdge == false
							|| (nestedResults.containsKey(history) && notNestedResults
									.containsKey(history))) {
						Map<TInputEdge, PureHistory> notNestedInputsMap = notNestedResults
								.get(history);
						Map<TInputEdge, PureHistory> nestedInputsMap = nestedResults
								.get(history);

						Collection<TInputEdge> edges = getInputEdges();
						for (TInputEdge e : edges) {
							Token t = e.getToken(e.isNested() ? nestedInputsMap
									.get(e) : notNestedInputsMap.get(e));
							fireInputDataMap.put(e.getLabel(), t.getData());
						}
						hist = history;
						break;
					} else if (hasNestedEdge && hasNotNestedEdge
							&& history.isSpecial()
							&& history.getCol().isEmpty()) {
						Map<TInputEdge, PureHistory> inputsMap = notNestedResults
								.get(history);
						for (TInputEdge e : inputsMap.keySet()) {
							Token t = e.getToken(inputsMap.get(e));
							fireInputDataMap.put(e.getLabel(), t.getData());
						}

						for (TInputEdge e : nestedEdges) {
							fireInputDataMap.put(e.getLabel(), new ArrayList());
						}

						hist = history.getHistory();
						break;
					} else if (hasNestedEdge && hasNotNestedEdge
							&& history.isSpecial()
							&& !history.getCol().isEmpty()
							&& nestedResults.containsKey(history.getHistory())) {
						Map<TInputEdge, PureHistory> notNestedinputsMap = notNestedResults
								.get(history);
						for (TInputEdge e : notNestedinputsMap.keySet()) {
							Token t = e.getToken(notNestedinputsMap.get(e));
							fireInputDataMap.put(e.getLabel(), t.getData());
						}

						Map<TInputEdge, PureHistory> nestedinputsMap = nestedResults
								.get(history.getHistory());
						for (TInputEdge e : nestedinputsMap.keySet()) {
							Token t = e.getToken(nestedinputsMap.get(e));
							fireInputDataMap.put(e.getLabel(), t.getData());
						}

						hist = history.getHistory();
						break;
					}
				}
			}

			if (hist == null) {
				return transitionFireResult;
			}

			// ODPALANIE TRANZYCJI!!
			transitionFireResult.isReadyToRun = true;
			transitionFireResult.inputData = fireInputDataMap;
			transitionFireResult.inputHistory = hist;

		} catch (Throwable e) {
			transitionFireResult.exception = e;
		}
		return transitionFireResult;
	}

	private TransitionFireResult tryToRunWithNoInput() {
		TransitionFireResult transitionFireResult = new TransitionFireResult(
				this);
		transitionFireResult.isReadyToRun = true;
		transitionFireResult.inputData = new HashMap<String, Object>();
		transitionFireResult.inputHistory = new History();
		return transitionFireResult;
	}

	@SuppressWarnings("unchecked")
	public final void run(TransitionFireResult transitionFireResult) {
		try {
			logger.info("Started transition execution : "+getName()+" id="+getId());
//			logger.debug("input data:\n"+IODataManager.getXMLRepresentaion(transitionFireResult.inputData));
			
			transitionFireResult.outputData = fire(transitionFireResult.inputData);

			logger.info("Finish transition execution: "+getName()+" id="+getId());
//			logger.debug("result:\n"+IODataManager.getXMLRepresentaion(transitionFireResult.outputData));
			transitionFireResult.endTime = new Date();
		} catch (Throwable e) {
			transitionFireResult.exception = e;
		}
	}

	@SuppressWarnings("unchecked")
	public final void finishRun(TransitionFireResult transitionFireResult,
			boolean disableTokens) {
		Object outputData = transitionFireResult.outputData;
		History h = transitionFireResult.inputHistory;


			boolean hasNestedEdge = false;
			boolean hasNotNestedEdge = false;
			for (TOutputEdge e : outputs) {
				hasNestedEdge = (hasNestedEdge || e.isNested());
				hasNotNestedEdge = (hasNotNestedEdge || !e.isNested());
			}

			// zapisz dane wyjsciowe
			for (TOutputEdge output : outputs) {
				Token token = null;
				if (hasNestedEdge && hasNotNestedEdge && !output.isNested()) {
					token = new Token(outputData, new History(outputData,
							(Collection) outputData, h));
				} else {
					token = new Token(outputData, h);
				}
				token.setActive(!disableTokens);
				output.setToken(token);
			}
	}

	// --------------------------------------------------- SAVE/LOAD
	// ------------------------------------------------- //
	/** default load * */
	public void load(Element elem) throws XMLLoaderException {
		this.id = elem.getAttributeValue("id"); // id ustawiany "recznie" - bez
		// sprawdzania idkow
		// //$NON-NLS-1$
		IdGenerator.registerId(this.id);

		Element tElem = elem.getChild("toolspecific"); //$NON-NLS-1$
		if (tElem != null) {
			setDescription(tElem.getChildText("description")); //$NON-NLS-1$
			setName(tElem.getChildText("name")); //$NON-NLS-1$
		}
	}

	/** default save * */
	public Element save() {
		Element elem = new Element("transition"); //$NON-NLS-1$
		elem.setAttribute("id", getId()); //$NON-NLS-1$
		elem.setAttribute("type", getTransitionType()); //$NON-NLS-1$

		Element toolspecificElem = new Element("toolspecific"); //$NON-NLS-1$
		toolspecificElem.setAttribute("tool", ""); //$NON-NLS-1$ //$NON-NLS-2$
		toolspecificElem.setAttribute("version", ""); //$NON-NLS-1$ //$NON-NLS-2$
		elem.addContent(toolspecificElem);

		Element dElem = new Element("description"); //$NON-NLS-1$
		dElem.addContent(getDescription());
		toolspecificElem.addContent(dElem);

		Element nElem = new Element("name"); //$NON-NLS-1$
		nElem.addContent(getName());
		toolspecificElem.addContent(nElem);

		return elem;
	}

	/**
	 * get the name of transition relative to TransitionFactory.class package or absolute
	 * @return
	 */
	private String getTransitionType() {
		if(this.getClass().getPackage().equals(TransitionFactory.class.getPackage())){
			String[] str = this.getClass().getName().split("[.]"); //$NON-NLS-1$
			return str[str.length - 1];
		}else{
			return this.getClass().getName();
		}
	}

	// --------------------------------------------------- OPISY
	// ------------------------------------------------- //
	/**
	 * pobranie opisu - jesli nie jest ustalony, to pobierany jest domyslny
	 * 
	 * @return
	 */
	public String getDescription() {
		return description != null ? description : getDefaultDescription();
	}

	/**
	 * ustawia opis
	 * 
	 * @param description
	 */
	public void setDescription(String description) {
		this.description = description;
	}

	/**
	 * pobiera opis IO
	 * 
	 * @return
	 */
	public String getIODescription() {
		StringBuffer res = new StringBuffer();
		res.append(Messages.BasicTransition_ioDescriptionInputs).append(
				getInputDescription());
		res.append(Messages.BasicTransition_ioDescriptionOutputs).append(
				getOutputDescription());
		return res.toString();
	}

	/**
	 * pobiera opis wyjsc
	 * 
	 * @return
	 */
	protected String getOutputDescription() {
		return getOutputTypeContainer().getType().toString();
	}

	// --------------------------------------------------- Nasluchiwacze
	// ---------------------------------------- //
	/**
	 * if the user initiation needed
	 * 
	 * @return
	 */
	public boolean isUserInitationNeeded() {
		return false;
	}

	@Override
	public String toString() {
		return Messages.BasicTransition_name + getId();
	}

	/**
	 * get description string of inputs
	 * 
	 * @return
	 */
	protected abstract String getInputDescription();

	/**
	 * fire transition with specific input data
	 * 
	 * @param inputMap -
	 *            input data map (input key -> value)
	 * @return result data
	 * @throws TransitionExecutionException
	 */
	protected abstract Object fire(Map<String, Object> inputMap)
			throws TransitionExecutionException;

	/**
	 * get default description
	 * 
	 * @return description
	 */
	public abstract String getDefaultDescription();

	/**
	 * add new input (name of input is the edge label e.getLabel)
	 * 
	 * @param e -
	 *            input edge
	 * @throws Exception
	 */
	public abstract void addInput(TInputEdge e) throws Exception;

	/**
	 * remove an input
	 * 
	 * @param key -
	 *            input key
	 */
	public abstract void removeInput(String key);

	/**
	 * disconnect from all inputs
	 */
	public abstract void disconnectAllInputs();

	/**
	 * get all input edges connected to this transition
	 * 
	 * @return collection of input edges
	 */
	public abstract Collection<TInputEdge> getInputEdges();

	/**
	 * how many input edges are connected
	 * 
	 * @return number of inputs
	 */
	public abstract int inputsCount();

	/**
	 * has free input of a given type
	 * 
	 * @param type -
	 *            type of an edge
	 * @return true/false
	 */
	public abstract boolean hasFreeInput(Type type);

	/**
	 * get all free inputs of a given type (to choose which one we want to use)
	 * 
	 * @param type -
	 *            type on an input
	 * @return collection of keys with a given input
	 */
	public abstract List<String> getFreeInputs(Type type);

	/**
	 * true if all input edges need to define their labels (usually false for
	 * one input transitions)
	 * 
	 * @return
	 */
	public abstract boolean isInputNameNeeded();

	/**
	 * checks if all inputs are already connected
	 * 
	 * @return true/false
	 */
	public abstract boolean areAllInputsDefined();
}
